package org.yaac.server.egql.evaluator;

import static com.google.common.collect.Lists.newArrayList;

import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import org.yaac.server.util.DatastoreUtil;
import org.yaac.shared.property.PropertyInfo;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.common.base.Objects;

/**
 * 
 * @author Max Zhu (thebbsky@gmail.com)
 * 
 */
public class EvaluationResult implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	/**
	 * result type
	 * 
	 * @author Max Zhu (thebbsky@gmail.com)
	 *
	 */
	private static enum Type {
		/**
		 * it's an entity property
		 */
		PROPERTY,
		
		/**
		 * calculated result, eg, 3 + a
		 * note that * selection is also considered as derived
		 */
		DERIVED,
	}
	
	private Type type;
	
	/**
	 * warnings in warning code bubbled from the whole evaluation tree
	 */
	private List<String> warnings;
	
	//==========================Property Path=============================
	/**
	 * 
	 * used to build property path when Type = Property
	 */
	private Key key;
	
	/**
	 * if it's a derived evaluation result, then propertyName = text expression, eg sum(*)
	 * if not, then propertyName = entity property name
	 * 
	 */
	private String propertyName;
	
	/**
	 * if it's in a list
	 * 
	 * this field will be null if it's a derived field
	 */
	private Integer index;
	
	//=========================Derived====================================
	private String title;
	
	//=========================Actual payload=============================
	/**
	 * actual content in Evaluation Type, eg, all numeric field will be BigDecimal here
	 * 
	 * evaluation type is different from result type, result types are consistent with datastore type
	 *  
	 */
	private Object payload;
	
	/**
	 * field evaluator will call this constructor, the native property
	 * 
	 * @param key
	 * @param propertyName
	 * @param index
	 * @param payload
	 */
	public EvaluationResult(Key key, String propertyName, Integer index, Object payload) {
		super();
		this.type = Type.PROPERTY;
		this.key = key;
		this.propertyName = propertyName;
		this.index = index;
		this.payload = DatastoreUtil.ensureEvaluationType(payload);	// convert from datastore type to evaluation type
		this.warnings = new LinkedList<String>();
	}

	
	/**
	 * other evaluators will call this constructor
	 * 
	 * @param payload
	 * @param previousResults
	 */
	public EvaluationResult(Object payload, EvaluationResult ...previousResults) {
		this(payload, newArrayList(previousResults));
	}

	/**
	 * other evaluators will call this constructor
	 * 
	 * @param payload
	 * @param previousResults
	 */
	public EvaluationResult(Object payload, Collection<EvaluationResult> previousResults) {
		super();
		
		this.type = Type.DERIVED;
		this.warnings = new LinkedList<String>();
		for (EvaluationResult previousResult : previousResults) {
			this.warnings.addAll(previousResult.warnings);
		}
		this.payload = payload;
	}
	
	public Key getKey() {
		return key;
	}

	public String getPropertyName() {
		return propertyName;
	}

	public Integer getIndex() {
		return index;
	}

	public Object getPayload() {
		return payload;
	}
	
	public List<String> getWarnings() {
		return warnings;
	}
	
	public EvaluationResult withWarning(String warning) {
		if (!warnings.contains(warning)) {
			warnings.add(warning);
		}
		return this;
	}
	
	public String getTitle() {
		return title;
	}

	public EvaluationResult withTitle(String title) {
		this.title = title;
		return this;
	}

	public void populatePropertyInfo(List<PropertyInfo> listToAppend) {
		Object val = this.getPayload();
		if (val instanceof EvaluationResult []) { // select all case, eg: select * from job
			for (EvaluationResult r : (EvaluationResult []) val) {
				r.populatePropertyInfo(listToAppend);
			}
		} else {
			listToAppend.add(DatastoreUtil.convert(this.keyString(), this.propertyTitle(), index, payload, warnings));
		}
	}

	private String keyString() {
		return this.type == Type.PROPERTY ? KeyFactory.keyToString(key) : null;
	}
	
	public String propertyTitle() {
		return this.type == Type.PROPERTY ? this.propertyName : this.title;
	}

	@Override
	public int hashCode() {
		return Objects.hashCode(this.type, this.warnings, this.payload, this.key, this.propertyName, this.index);
	}

	@Override
	public boolean equals(Object obj) {
		// note that title is not part of equals function
		// evaluation results with same value but different title are still considered as same
		if (obj instanceof EvaluationResult) {
			return Objects.equal(this.type, ((EvaluationResult) obj).type) 
				&& Objects.equal(this.warnings, ((EvaluationResult) obj).warnings)
				&& Objects.equal(this.payload, ((EvaluationResult) obj).payload)
				&& Objects.equal(this.key, ((EvaluationResult) obj).key)
				&& Objects.equal(this.propertyName, ((EvaluationResult) obj).propertyName)
				&& Objects.equal(this.index, ((EvaluationResult) obj).index);
		} 
		
		return false;
	}

	@Override
	public String toString() {
		return Objects.toStringHelper(this)
			.add("type", type)
			.add("warnings", warnings)
			.add("payload", payload)
			.add("key", key)
			.add("propertyName", propertyName)
			.add("index", index)
			.add("title", title)
			.toString();
	}
}